Rendering Solid Objects - The Four Technical Challenges

by Tomas J. Nally

Steelweaver52@aol.com


Home

Using QCard DLL - Lesson 3 - Watson

Sprite Byte - Watson

Programming a Word Game - Terra

Maven Puzzle Contest - Terra

Adding an Icon to the Taskbar - Lewis

Beginning Programming IX - Moore

Rendering Solid Objects - Nally


Submission Guildlines

Newsletter Help

Index

Introduction

As frequent readers of this Newsletter may have observed, one of my programming interests is vector graphics. I've written about drawing arcs; creating "rubber band" objects; mapping real world objects to the screen; and three articles about wire model images of 3D objects. I've also dabbled in bitmap graphics with an article and application called "Symmetrical Paint".

Though I've always been intrigued and impressed by computer images of solid objects, I've refrained from attempting to render solid objects myself. The burden of assembling and organizing the data seemed severe, and the algorithms were probably too complex.

Lately, however, I was motivated to give solid object imaging a try. It just so happened that I was browsing the programming books at Barnes & Noble, and I read a few pages of a particular book which made the project seem doable.

What I will write about below is definitely not the only way to create images of solid objects. And the procedure that I will describe certainly doesn't produce the most realistic images of solid objects. However, it might be the simplest way, which is why it appealed to me.

But before we get into that, it might be convenient to select a name for this rendering method. Because the method involves defining objects using triangles, then sorting the triangles prior to plotting, let's call it "MST" for the Method of Sorted Triangles. It might sound a little funny, but you will observe that the name does capture the method.

The Technical Challenges of Solid Modeling

Lame Solid Imager

Along with this discussion, I'm providing a Liberty BASIC program, Lame Solid Imager (LSI), which is released as open source. Readers are free to use the code in their own programs, provided that the copyright is respected. Read more about Lame Solid Imager at the end of this article.

Rather than discuss the particulars of Lame Solid Imager's code, however, I prefer to discuss the four technical challenges that must be met in order accomplish rendering by MST. That way, the ambitious programmer can write a better imaging program by amassing her programming talents, isolating the challenges from each other, and tackling them one by one.

So without further adieu, let's identify the challenges.

Challenge No. 1: Defining Object Surfaces With Data

To render with MST, the first thing that the programmer has to do is to define the surfaces of an object with numerical data. As you can see from the screen capture on the right, I chose to divide the solid objects into component triangles (which I also call facets). Triangles seemed to be a good choice because each component triangle can be defined by a minimum amount of data.

In particular, examine the gray cylinder in the screen capture. Though you cannot see them all, this cylinder is defined by 80 triangular-shaped facets. Each triangle is defined by three points (or "nodes") in space. Note that adjacent triangles can have one or two nodes in common. So, despite the fact that the cylinder is composed of 80 component triangles, those 80 triangles do not require 240 unique nodes. Because adjacent triangles share nodes, this cylinder only requires 49 unique nodes.

Each node, of course, represents a position in space. Therefore, each of the 49 nodes require an x-, a y- and a z-coordinate. If we were to put the data for the first six nodes in tabular form, it might look something like this:

Node
ID      x        y         z
-------------------------------
1     15.00     0.00     30.000
2     13.86     0.00     35.740
3     10.61     0.00     40.607
4      5.74     0.00     43.858
5      0.00     0.00     45.000
6     -5.74     0.00     43.858
-------------------------------

With the nodes defined, the programmer is now in a position to define all of the component triangles that make up the cylinder. A triangle is defined by identifying the three nodes that form the vertices of the triangle. A table of the triangular data might look similar to the table below:

Triangle
ID    i-node  j-node  k-node
----------------------------
1      1       2       17
2      2       3       18
3      3       4       19
4      4       5       20
5      5       6       21
6      6       7       22
----------------------------

One might start to see how organizing the data in this fashion will allow us to draw the objects. For example, once it is time to draw triangle No. 4, you know that the program has to address nodes 4, 5, and 20. Further, because of the data in the nodal table, you know exactly where node 4 is located in space, as well as node 5 and node 20.

Each component triangle may have other properties in addition to the three nodes. For example, I also give the triangles additional data to describe facet color and edge color. The programmer may think of other attributes that she wants each triangle to own.

Arrays, of course, are the natural programming structure in which to hold this data. I prefer to use two dimensional string arrays to hold both nodal data and facet data.

By the way, how is the raw data for an object generated? The logical way to generate data would be to use Liberty BASIC programming. For instance, if the user provided a cylinder height, a cylinder diameter and a color, that should be enough information for the programmer to generate all the raw data for all 49 nodes and 80 facets of the cylinder.

I have not done that yet for LSI, however. I used Microsoft Excel to generate the data for all 5 objects in the LSI demo. I used Excel because it is perfectly built for this activity. It allowed me to spend my programming time developing the rendering engine for LSI.

Challenge No. 2: Sorting Facets in Descending Order, Based On Facet Distance From the Camera

If you've read the articles on wire model imaging linked above, you will be familiar with the fact that 3D rendering requires that the camera location in space be specified. This is critical because the arrangement of objects in the view depends on camera location.

Understand also that objects nearer to the camera must render themselves in front of other objects that are farther away. To carry this principle even further, portions of the same object that are nearer the camera must render themselves in front of portions of the same object that are further away.

This elemental principle suggests that all of the facets of all of the objects in any scene must be sorted. In fact, in the MST method, triangular facets must be sorted in descending order beginning with the facets that are furthest from the camera, and ending with the facets that are nearest the camera. Later, this sorted order will also become the plot order.

But exactly how far is any random facet--say facet No. 4--from the camera? The camera is already located at some point in space with x-, y- and z-coordinates. In fact, I usually call these coordinates CamX, CamY and CamZ.

A facet, on the other hand, does not occupy a single point in space. After all, a facet is a triangle. However, we can find the approximate centerpoint of each triangular facet by finding the average of the x-coordinates of the facet's i-node, j-node and k-node; the average of the y-coordinates of the facet's i-node, j-node and k-node; and the average of the z-coordinates of the facet's i-node, j-node and k-node. We might refer to the coordinates of this approximate center point as AvgX, AvgY and AvgZ.

The distance, then, between the camera location and the centerpoint of this facet can be found by using the Pythagorean Theorem:

Distance = SQR((CamX - AvgX)^2 + (CamY - AvgY)^2 + (CamZ - AvgZ)^2)

This procedure must be carried out for each and every facet. If there are 6,000 facets in your scene, then this procedure must be executed 6,000 times. Once this is accomplished, sort the facets by distance using your favorite sorting algorithm.

Understand also that it may be inconvenient to sort the original, raw facet data. Your program may require that facet No. 1 always be facet No. 1. If that's the case, then sorting facets by distance will disturb the natural order of the facets.

What I've done in LSI is to create a parallel array holding all of the facet distances from the camera. Instead of sorting the original, raw facet data, I sort all of the data in this parallel array, both the facet index and the facet's distance from the camera. This operation may show that, say, facet No. 82 of 600 is furthest from the camera, and must therefore be plotted first. Yet, the original facet data remains in its original order.

Even though we have a sort order for the facets, we cannot plot them yet. This is because all the nodes of all the facets still exist in 3D space. Somehow, we need to project all of this 3D nodal data onto our 2D GRAPHICBOX, and give all the nodes 2D screen coordinates. That is the nature of the next technical challenge.

Challenge No. 3: Projecting Each Facet Onto An Image Plane, and Giving it Screen Coordinates

Say that you have a point in space that exists at x = 500, y = 1250 and z = 4300. How do you plot this point in a Liberty BASIC GRAPHICBOX?

You can't yet--at least not without converting the coordinates of this point into a different, two-dimensional form. The process of converting a point in 3D space involves projecting this point onto a 2D image plane. This itself is a formidable challenge involving vector mathematics, and merits its own series of articles.

Fortunately, a mechanism for dealing with this challenge has already been provided in previous issues of the Liberty BASIC Newsletter. In fact, if you surf to this article in issue NL113, you can read about two user-defined Liberty BASIC functions called ScreenX() and ScreenY(). These two functions were written for the express purpose described above: projecting the nodes of 3D objects onto a 2D "image plane", and then converting that information into GRAPHICBOX coordinates.

ScreenX() and ScreenY() are not very efficient. Yet they have proven themselves to be quite durable over time, and they form part of the core programming of Lame Solid Imager.

Now, to project a facet onto the image plane and give it screen coordinates, one must project the three nodes of each and every facet. Since each facet is defined by a unique set of three nodes, calculating screen coordinates for each node essentially accomplishes the task of projecting the image of the facet onto the screen.

Once x- and y- screen coordinates for each and every node have been determined, Technical Challenge No. 3 is essentially finished. It is now time to plot the triangular facets to the screen in sorted order.

Challenge No. 4: Plotting the Triangular Facets to the Screen in the Sorted Order

Let's use a bullet list to review the essential ideas of MST discussed so far:

Those computing activities described above set the stage for the final challenge: plotting the triangular facets to the screen in sorted order.

First, understand the reason for sorting the facets based on their distance from the camera: we need to plot the furthest facet first, then the next furthest, and the next. The last facet plotted should be the one nearest the camera. This ensures that near objects will appear in front of the more distant objects.

Plotting sorted triangles

So, how does one plot a triangular facet? Recall that Challenge 3 gives screen coordinates to each of the three nodes that make up a facet. For example, triangular facet No. 36, may have the screen coordinates shown in the table below, and also shown in the upper half of the figure on the right.

Data for facet No. 36

           ScreenX   ScreenY
----------------------------
i-node      61         41
j-node      47        132
k-node     141         92
----------------------------

Challenge No. 4 requires that the screen coordinates for facet 36 be sent to a function or subroutine which can plot a filled triangle based on the x,y coordinates of the three nodes of that triangle. In Lame Solid Imager, I use a function to plot the triangles. The arguments of the function look like this:

Function PlotTriangle(x1,y1,x2,y2,x3,y3,LineColor1$,LineColor2$,LineColor3$,FillColor$)

Note that I am not only sending the plot function the coordinates of the nodes of the triangle, but I'm also sending it the colors of the three edges of the triangle, as well as the fill color of the triangle. That way, my function has the flexibility to plot the edges of the triangle in three different colors if I desire. Or, if I do not want the edges of my triangle to stand out, then I can plot them using the same color as the fill color.

Understand this key idea also: it is unlikely that you can use a generic fill command to plot a filled triangle. This is because the fill process must fill the entire interior of the triangle regardless of any lit pixels that may already be in the interior of the triangle. If your triangle plotting routine is unable to do this, then you will be unable to demonstrate the one facet exists in front of another facet.

For me, plotting filled triangles was a difficult challenge. As you are processing the geometry of a triangle, watch out for division by zero errors which may occur when one node is directly above another node.

Demo Program: Lame Solid Imager

Lame Solid Imager is a Liberty BASIC program that can be found in the archive for this newsletter, nl124.zip. It can be found in the zip archive under the name LSI.bas. It consists of a BASIC program only. There are no support files, such as bitmaps or text files.

Lame Solid Imager produces nice-looking plots of 5 solid objects. Beyond that, it has very few features, so it would be stretching things to refer to it as a "CAD" program. I haulted the development of LSI when it had advanced far enough to enable me to discuss the four challenges.

Among the limitations of LSI are these:

  1. There are no provisions for creating new objects. LSI is essentially a viewing program for the five objects whose data already resides within the source code of LSI.

  2. It contains no help file.

  3. All the limitations of ScreenX() and ScreenY() apply to Lame Solid Imager. You can read those limitations here. An important one is this: ScreenX() and ScreenY() make no distinction between objects that reside in front of the camera, and objects that reside to the rear of the camera. It will project all nodes to the image plane, producing bizarre output. For this reason, the camera shouldn't be placed in the midst of the objects. Rather, it should be placed outside of the objects. If you place the camera in the midst of the objects, the component triangles will engulf the entire graphic box, and may take hours to plot. If you see that happening, it's better just to attempt to exit the program while the plot is in progress.

  4. LSI is not really built to understand objects which intersect each other. Triangles are triangles. It has no way of understanding when one object passes through another object. Instead, it will relentlessly follow the rule of plotting distant triangles first, and nearby triangles last. It won't subdivide any triangles if two objects intersect each other.

Regarding the last item, I will say this, however: if all component triangles were infinitely small, then LSI could successfully plot objects which intersect each other. Of course, the data handling and plotting process would take so long that the usefulness of LSI would go to zero.

License

I am releasing Lame Solid Imager as open source while retaining the copyright. Interested programmers may use any and all of the source code of LSI in their own programs, as long as the attribution at the top of the source code remains.


Home

Using QCard DLL - Lesson 3 - Watson

Sprite Byte - Watson

Programming a Word Game - Terra

Maven Puzzle Contest - Terra

Adding an Icon to the Taskbar - Lewis

Beginning Programming IX - Moore

Rendering Solid Objects - Nally


Submission Guildlines

Newsletter Help

Index